﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

namespace GerritDrost.VirtualBoxManager
{
    public class VirtualMachineManager
    {
        // Path of the VirtualBox binaries.
        // This is required for Windows, in Linux
        // these binaries are in the path most of the times.
        private string virtualboxPath;

        // the number of milliseconds between updating the VM info
        private int pollInterval;

        // represents the state of the timer that does the polling
        private bool polling;

        // Datastructures in which the VirtualMachine instances are stored/looked up.
        private Dictionary<string, VirtualMachine> virtualMachineMap;
        private List<VirtualMachine> virtualMachines;

        // Lock object, since the timer runs on a different thread, we use this 
        // to make sure we have no cross-threading issues.
        private object lockObject = new object();

        // This timer will periodically poll Virtualbox
        private Timer virtualboxPollTimer;

        // Event that is raised when the virtual machine list has been updated
        public delegate void MachinesUpdatedDelegate();
        public event MachinesUpdatedDelegate MachinesUpdated;

        // These variables represent the calls to the cli of Virtualbox.
        ProcessStartInfo listVMsInfo;
        ProcessStartInfo listRunningVMsInfo;
        private string p;

        /// <summary>
        /// Constructs and initializes the virtualbox manager.
        /// </summary>
        /// <param name="virtualboxPath">
        /// Path of the VirtualBox binaries.
        /// This is required for Windows, in Linux these binaries can be found
        /// in the path most of the times.
        /// </param>
        /// <param name="pollInterval">
        /// Interval in ms between two polls to Virtualbox to update the virtual machine statuses.
        /// </param>
        public VirtualMachineManager(string virtualboxPath, int pollInterval)
        {
            // set the virtualbox path
            this.virtualboxPath = virtualboxPath;

            // store the given poll interval
            this.pollInterval = pollInterval;

            // Create ProcessStartInfo instances for the periodic calls to list all (running) virtualboxes.
            // This one is for polling all EXISTING virtual machines
            listVMsInfo = new ProcessStartInfo();
            listVMsInfo.FileName = Path.Combine(virtualboxPath, "VBoxManage");
            listVMsInfo.Arguments = "list vms";
            listVMsInfo.CreateNoWindow = true;
            listVMsInfo.RedirectStandardOutput = true;
            listVMsInfo.UseShellExecute = false;
            // This one is for polling all RUNNING virtual machines
            listRunningVMsInfo = new ProcessStartInfo();
            listRunningVMsInfo.FileName = Path.Combine(virtualboxPath, "VBoxManage");
            listRunningVMsInfo.Arguments = "list runningvms";
            listRunningVMsInfo.CreateNoWindow = true;
            listRunningVMsInfo.RedirectStandardOutput = true;
            listRunningVMsInfo.UseShellExecute = false;

            //set up a timer
            virtualboxPollTimer = new Timer(new TimerCallback((object stateInfo) => UpdateVirtualMachines()), null, Timeout.Infinite, Timeout.Infinite);

            //update once
            UpdateVirtualMachines();
        }

        /// <summary>
        /// Returns the state of the polling timer. True when it is polling, false otherwise.
        /// </summary>
        public bool IsPolling
        {
            get
            {
                return polling;
            }
        }

        /// <summary>
        /// Starts the timer to poll at the set interval
        /// </summary>
        public void StartPolling()
        {
            virtualboxPollTimer.Change(0, pollInterval);
            polling = true;
        }

        /// <summary>
        /// Stops the timer to cease the polling.
        /// </summary>
        public void StopPolling()
        {
            virtualboxPollTimer.Change(Timeout.Infinite, Timeout.Infinite);
            polling = false;
        }

        /// <summary>
        /// Property for the PollInterval. When the timer is currently polling, it updates the interval while running.
        /// </summary>
        public int PollInterval
        {
            get
            {
                return pollInterval;
            }
            set
            {
                pollInterval = value;

                if (polling)
                    virtualboxPollTimer.Change(0, pollInterval);
            }
        }

        /// <summary>
        ///  Uses Virtualbox's cli to get a list of VMs and their states. When it's done, raises an event about the possible changes. This method is also used by the polling timer built into this class.
        /// </summary>
        public void UpdateVirtualMachines()
        {
            // call the Virtualbox CLI tools and process the results into an array of lines, since each line represents one VM
            string[] machineStrings = RunAndReadProcess(listVMsInfo).Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            string[] runningMachineStrings = RunAndReadProcess(listRunningVMsInfo).Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

            //These variables will be filled with the information and will replace the current ones
            List<VirtualMachine> machines = new List<VirtualMachine>();
            Dictionary<string, VirtualMachine> machinesMap = new Dictionary<string, VirtualMachine>();

            //Local list which will contain UUIDs of running VMs
            List<string> runningMachines = new List<string>();

            // Processes the array of strings representing running VMs: retreives the UUIDs and stores them in the list. 
            foreach (string machineString in runningMachineStrings)
            {
                string uuid = machineString.Substring(machineString.LastIndexOf("{")).Trim(new char[] { '{', '}' });
                runningMachines.Add(uuid);
            }

            // Iterates through strings representing all VMs, also those that are not running.
            // Creates VirtualMachine objects for them and adds them to the list/dictionary. Sets the
            // running flag depending on whether or not they are running according to the runningMachines list.
            foreach (string machineString in machineStrings)
            {
                int uuidIndex = machineString.LastIndexOf("{");
                string uuid = machineString.Substring(uuidIndex).Trim(new char[] { '{', '}' });
                string name = machineString.Substring(0, uuidIndex - 1).Trim(new char[] { '"' });

                VirtualMachine machine = new VirtualMachine()
                {
                    Uuid = uuid,
                    Name = name,
                    Running = runningMachines.Contains(uuid)
                };

                machines.Add(machine);
                machinesMap.Add(uuid, machine);
            }

            // Since this method might not run on the main thread, we use a lock when accessing the list/dictionary stored in this class to ensure nothing strange happens.
            lock (lockObject)
            {
                this.virtualMachines = machines;
                this.virtualMachineMap = machinesMap;
            }

            // Raise the event if there are subscriptions
            if (MachinesUpdated != null)
                MachinesUpdated();

        }

        /// <summary>
        /// Returns a list of all currently listed virtual machines. When polling is turned off, this can be out of date.
        /// </summary>
        public List<VirtualMachine> VirtualMachines
        {
            get { lock (lockObject) { return virtualMachines; } }
        }

        /// <summary>
        /// Returns a virtual machine by its UUID or null if it can't be found.
        /// </summary>
        /// <param name="uuid">the UUID of the virtual machine.</param>
        /// <returns></returns>
        public VirtualMachine GetVirtualMachine(string uuid)
        {
            lock (lockObject)
            {
                return virtualMachineMap[uuid];
            }
        }

        /// <summary>
        /// Tries to resume the virtual machine. Does nothing if the UUID can't be found.
        /// </summary>
        /// <param name="uuid">the UUID of the virtual machine.</param>
        public void ResumeVirtualMachine(string uuid)
        {
            lock (lockObject)
            {
                VirtualMachine machine = virtualMachineMap[uuid];

                if (machine == null)
                    return;

                ProcessStartInfo resumeVMInfo = new ProcessStartInfo();
                resumeVMInfo.FileName = Path.Combine(virtualboxPath, "VBoxHeadless");
                resumeVMInfo.Arguments = String.Format(" --startvm {0}", uuid);
                resumeVMInfo.CreateNoWindow = true;
                resumeVMInfo.RedirectStandardOutput = true;
                resumeVMInfo.UseShellExecute = false;

                RunProcess(resumeVMInfo);
            }
        }

        /// <summary>
        /// Tries to suspend the virtual machine. Does nothing if the UUID can't be found.
        /// </summary>
        /// <param name="uuid">the UUID of the virtual machine.</param>
        public void SuspendVirtualMachine(string uuid)
        {
            lock (lockObject)
            {
                VirtualMachine machine = virtualMachineMap[uuid];

                if (machine == null)
                    return;

                ProcessStartInfo suspendVMInfo = new ProcessStartInfo();
                suspendVMInfo.FileName = Path.Combine(virtualboxPath, "VBoxManage");
                suspendVMInfo.Arguments = String.Format(" controlvm {0} savestate", uuid);
                suspendVMInfo.CreateNoWindow = true;
                suspendVMInfo.RedirectStandardOutput = true;
                suspendVMInfo.UseShellExecute = false;

                RunProcess(suspendVMInfo);
            }
        }

        /// <summary>
        /// Runs a process without waiting for it to finish.
        /// </summary>
        /// <param name="startInfo">the ProcessStartInfo object describing the process that needs to be executed</param>
        private static void RunProcess(ProcessStartInfo startInfo)
        {
            Process process = new Process();
            process.StartInfo = startInfo;
            process.Start();
        }

        /// <summary>
        /// Runs a process, waits for it to finish and returns its commandline output.
        /// </summary>
        /// <param name="startInfo">the ProcessStartInfo object describing the process that needs to be executed</param>
        /// <returns></returns>
        private static string RunAndReadProcess(ProcessStartInfo startInfo)
        {
            Process process = new Process();
            process.StartInfo = startInfo;
            process.Start();
            process.WaitForExit();
            string output = process.StandardOutput.ReadToEnd();
            return output;
        }
    }
}
